package ro.isdc.navigator.retriever.jpa;

import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;

import ro.isdc.navigator.retriever.IRetriever;

/**
 * The JPA retriever, responsible for creating, for populating and
 * for running JPQLs.
 * 
 * @author Andrei Chirila / ISDC! Romania
 * @version 1.0
 * @date Jul 10, 2009
 */
public class JPARetriever<T> implements IRetriever<T>{

  private static final String STANDARD_JPQL_STRING = "from .*";

  private static final Pattern pattern = Pattern.compile(STANDARD_JPQL_STRING);

  
  /**
   * The JPQL.
   */
  private String JPQL;
  
  /**
   * Arguments names.
   */
  private List<String> argsNames;
  
  /**
   * The entity manager.
   */
  @PersistenceContext
  private EntityManager entityManager;
  
  /**
   * @param jpql the jPQL to set
   */
  public void setJPQL(final String jpql) {
    this.JPQL = jpql;
  }
  
  /**
   * @param argsNames the argsNames to set
   */
  public void setArgsNames(final List<String> argsNames) {
    this.argsNames = argsNames;
  }
  
  @Override
  public long getNumberOfResults(final List<Object> args) {
    Query query = entityManager.createQuery(buildCountJPQL());
    prepareQueryParameters(args, query);
    
    return (Long) query.getSingleResult();
  }

  /**
   * Build a count JPQL from the original JPQL.
   * 
   * @return the count JPQL.
   */
  private String buildCountJPQL() {
    Matcher matcher = pattern.matcher(JPQL);
    matcher.find();
    return "select count(*) " + matcher.group();
  }

  /**
   * {@inheritDoc}
   */
  @SuppressWarnings("unchecked")
  @Override
  public List<T> getResults(final int startIndex, final int blockSize, 
      final List<Object> args) {
    Query query = prepareQuery(startIndex, blockSize);
    prepareQueryParameters(args, query);
    return query.getResultList();
  }

  /**
   * Prepares the actual query when given the start index and the size of the
   * block of elements to retrieve.
   * 
   * @param startIndex
   *          the start index
   * @param blockSize
   *          the block size
   * @return the resulted Query object
   */
  private Query prepareQuery(final int startIndex, final int blockSize) {
    Query query = entityManager.createQuery(JPQL);
    query.setFirstResult(startIndex);
    query.setMaxResults(blockSize);
    return query;
  }
  
  /**
   * Injects the parameters into the query.
   * 
   * @param args
   *          the arguments
   * @param query
   *          the query to populate/inject with parameters
   */
  private void prepareQueryParameters(final List<Object> args, final Query query) {
    if (argsNames == null || argsNames.isEmpty()) {
      prepareIndexedQueryParameters(args, query);
    } else {
      prepareNamedQueryParameters(args, query);
    }
  }

  /**
   * Prepares the query when the parameters are index-based.
   * @param args the arguments to be added
   * @param query the query to be injected with the indexed parameters
   */
  private void prepareNamedQueryParameters(final List<Object> args,
      final Query query) {
    if (args == null || args.isEmpty()) {
      throw new IllegalStateException(
          "You haven't specified any arguments in \"JPQL\" although you have aguments names!");
    }
    
    if (argsNames.size() != args.size()) {
      throw new IllegalStateException(
          "The number of param values is not equal to the number of the param names in \"JPQL\"");
    }

    String[] paramNames = argsNames.toArray(new String[argsNames.size()]);
    Object[] paramValues = args.toArray();
    for (int i = 0; i < paramValues.length; i++) {
      query.setParameter(paramNames[i], paramValues[i]);
    }
  }

  /**
   * Prepares the query when the parameters are name-based.
   * 
   * @param args the arguments to be added
   * @param query the query to be injected with the named parameters
   */
  private void prepareIndexedQueryParameters(final List<Object> args,
      final Query query) {
    if (args != null) {
      Object[] paramValues = args.toArray();
      for (int i = 0; i < paramValues.length; i++) {
        query.setParameter(i+1, paramValues[i]);
      }
    }
  }
    
}
